//	---------------------------------------------------------------------------
//	jWebSocket - WebSocket Client for Java Micro Edition (J2ME)
//	Copyright (c) 2010 jWebSocket.org, Alexander Schulze, Innotrade GmbH
//	---------------------------------------------------------------------------
//	This program is free software; you can redistribute it and/or modify it
//	under the terms of the GNU Lesser General Public License as published by the
//	Free Software Foundation; either version 3 of the License, or (at your
//	option) any later version.
//	This program is distributed in the hope that it will be useful, but WITHOUT
//	ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
//	FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
//	more details.
//	You should have received a copy of the GNU Lesser General Public License along
//	with this program; if not, see <http://www.gnu.org/licenses/lgpl.html>.
//	---------------------------------------------------------------------------
package com.msstudio.websocket.client;

import java.io.InputStream;
import java.io.OutputStream;
import javax.microedition.io.Connector;
import javax.microedition.io.SocketConnection;
import com.msstudio.websocket.client.api.WebSocketPacket;
import com.msstudio.websocket.client.exception.WebSocketException;
import com.msstudio.websocket.client.api.WebSocketHandshake;
import com.msstudio.websocket.client.event.WebSocketClientEvent;

/**
 *
 * @author aschulze
 */
public class WebSocketClient extends BaseClient {

    private final static byte OPCODE_CONT = 0x0;
    private final static byte OPCODE_TEXT_FRAME = 0x1;
    private final static byte OPCODE_BINARY_FRAME = 0x2;
    //private final static byte OPCODE_CLOSEDFROMCLIENT = 0x8;
    private final static byte OPCODE_PING = 0x9;
    private final static byte OPCODE_PONG = 0xA;
    private final static byte STATE_FRAME_START = 0;
    private final static byte STATE_MASKING_KEY = 1;
    private final static byte STATE_PAYLOAD = 2;
    private final static byte STATE_CORRUPT = 3;
    private boolean mIsRunning = false;
    private Thread mInboundThread = null;
    private SocketConnection mSocket = null;
    private InputStream mIn = null;
    private OutputStream mOut = null;
    private boolean canSend = true;
    private byte mCurrentState = STATE_FRAME_START;
    private boolean mFrameFin;
    private int mFrameOpcode;
    private int mFramePayloadLen;
    private byte[] mMaskKeys = new byte[4];
    private int mPayloadIndex = 0;
    private boolean mFrameMasked = false;
    private byte[] mPayloadBuffer = null;
    
    public final static int DISCONNECTED = 0;
    public final static int CONNECTED = 1;
    public final static int AUTHENTICATED = 2;
    private String mUsername = null;

    public WebSocketClient() {
    }

    public void open(String aURL, String webSocketPath) throws WebSocketException {
        try {
            // validate URL, Java ME expects socket:// 
            if (aURL.startsWith("ws://")) {
                aURL = aURL.substring(5);
            } else if (aURL.startsWith("http://")) {
                aURL = aURL.substring(7);
            }
            mSocket = (SocketConnection) Connector.open("socket://" + aURL, Connector.READ_WRITE);
            // "socket://localhost:8787"

            mIn = mSocket.openInputStream();
            mOut = mSocket.openOutputStream();

            // send handshake to server，只有websocket 10才支持大于256位的二进制传输，我们要传语音流，必须要用ver10
            byte[] lReq = //WebSocketHandshake.generateC2SRequest(aURL, webSocketPath);
                    WebSocketHandshake.generateWebsocket10Handshake(aURL, webSocketPath);
            mOut.write(lReq);
            mOut.flush();

            // wait on handshake response
            //byte[] lBuff = WebSocketHandshake.readS2CResponse(mIn);
            WebSocketHandshake.readS2CResponse(mIn);
            // 不用解析返回的回应了，如果不是有效websocket server,后面也没法继续
            // Map lResp = WebSocketHandshake.parseS2CResponse(lBuff);

            canSend = true;
            mInboundThread = new Thread(new InboundProcess());
            mInboundThread.start();
        } catch (Exception ex) {
            this.close();
            throw new WebSocketException(ex.getClass().getName() + " when opening WebSocket connection: " + ex.getMessage());
        }
    }

    public void send(String aData, String aEncoding) throws WebSocketException {
        byte[] msgBytes = null;
        try {
            /* mOut.write(0x00);
            mOut.write(aData.getBytes(aEncoding));
            mOut.write(0xff);
            mOut.flush(); */
            msgBytes = aData.getBytes(aEncoding);
        } catch (Exception ex) {
            throw new WebSocketException("Failed to encode: " + ex.getMessage());
        }
        sendHybiFrame(msgBytes, OPCODE_TEXT_FRAME, true);
        msgBytes = null;
    }
    
    public void send(String aString) throws WebSocketException {
        send(aString, "UTF-8");
    }

    public void send(byte[] msgBytes) throws WebSocketException {
        // https://github.com/Treegger/websocket-client/blob/master/java/src/main/java/com/treegger/websocket/WSConnector.java       
        // http://www.oschina.net/question/163220_34853
        /* try {
        mOut.write( 0x80 );    		
        int dataLen = msgBytes.length;    		
        mOut.write((byte) (dataLen >>> 28 & 0x7F | 0x80));    		
        mOut.write((byte) (dataLen >>> 14 & 0x7F | 0x80));    		
        mOut.write((byte) (dataLen >>> 7 & 0x7F | 0x80));    		
        mOut.write((byte) (dataLen & 0x7F));    		
        mOut.write( msgBytes );    		
        mOut.flush();
        } catch (Exception ex) {
        throw new WebSocketException(ex.getClass().getName() + " when sending via WebSocket connection: " + ex.getMessage());
        // System.out.println(ex.getClass().getSimpleName() + ": " + ex.getMessage());
        } */
        sendHybiFrame(msgBytes, OPCODE_BINARY_FRAME, true);
    }

    /*
     * fin = %x0 ; 表示这不是当前消息的最后一帧，后面还有消息  
     *       %x1 ; 表示这是当前消息的最后一帧  
     * frame-opcode            = %x0 ; 表示这是一个连续帧消息  
    / %x1 ; 表示文本消息  
    / %x2 ; 表示二进制消息  
    / %x3-7 ; 保留  
    / %x8 ; 表示客户端发起的关闭  
    / %x9 ; ping（用于心跳）  
    / %xA ; pong（用于心跳） 
     */
    public void sendHybiFrame(byte[] msgBytes, byte opcode, boolean fin) throws WebSocketException {
        if (!canSend) {
            return;
        }
        int length = msgBytes.length;
        if (opcode == OPCODE_PING && length > 125) {
            throw new WebSocketException("invalid payload for PING (payload length must be <= 125, was " + length);
        }

        canSend = false;
        byte b0 = 0;
        if (fin) {
            b0 |= (1 << 7);
        }
        b0 |= opcode;
        try {
            mOut.write(b0);
            if (length <= 125) {
                mOut.write((byte) (length | 128)); // 服务器端mask可以为0，客户端必须为1
            } else if (length <= 0xFFFF) {
                mOut.write((byte) 254); //(126|128)); // 服务器端mask可以为0，客户端必须为1
                mOut.write((byte) ((length >>> 8) & 0xFF));
                mOut.write((byte) ((length) & 0xFF));
            } else {
                mOut.write((byte) 255); //(127|128)); // 服务器端mask可以为0，客户端必须为1
                mOut.write((byte) 0);
                mOut.write((byte) 0);
                mOut.write((byte) 0);
                mOut.write((byte) 0);
                mOut.write((byte) ((length >>> 24) & 0xFF));
                mOut.write((byte) ((length >>> 16) & 0xFF));
                mOut.write((byte) ((length >>> 8) & 0xFF));
                mOut.write((byte) ((length) & 0xFF));
            }
            byte maskKey = (byte) 88; // mask key应该为4字节的，为了好计算，将四个字节都用同一值
            mOut.write((byte) maskKey);
            mOut.write((byte) maskKey);
            mOut.write((byte) maskKey);
            mOut.write((byte) maskKey);
            mOut.flush();
            for (int i = 0; i < length; i++) {
                mOut.write((byte) (msgBytes[i] ^ maskKey));
            }
            mOut.flush();
        } catch (Exception ex) {
            throw new WebSocketException("Failed to send: " + ex.getMessage());
        } finally {
            canSend = true;
        }
    }

    public void close() {
        mIsRunning = false;
        canSend = false;
        // 下列的interrupt, close操作不要放在同一个catch里面，否则如果有异常会导致其余的没有正常关闭，导致内存泄漏 ！！！
        if (mInboundThread != null && mInboundThread.isAlive()) {
            try {
                mInboundThread.interrupt();
            } catch (Exception ex) {
            }
        }
        mInboundThread = null;
        try {
            if (mOut != null) {
                mOut.close();
            }
        } catch (Exception ex) {
        }
        mOut = null;
        try {
            if (mIn != null) {
                mIn.close();
            }
        } catch (Exception ex) {
        }
        mIn = null;
        try {
            if (mSocket != null) {
                mSocket.close();
            }
        } catch (Exception ex) {
        }
        mSocket = null;
        notifyClosed(new WebSocketClientEvent());
    }

    private class InboundProcess implements Runnable {

        public void run() {
            mIsRunning = true;
            mCurrentState = STATE_FRAME_START;
            byte[] lBuff = new byte[MAX_FRAME_SIZE];
            WebSocketClientEvent lEvent = new WebSocketClientEvent();
            notifyOpened(lEvent);
            while (mIsRunning) {
                decodeHybiFrame();
            }
        }
    }

    private void decodeHybiFrame() {
        byte len1, len2, len3, len4, len5, len6, len7, len8;
        try {
            switch (mCurrentState) {
                case STATE_FRAME_START: { // FIN, RSV, OPCODE
                    // 初始化需要的内容
                    mPayloadBuffer = null;

                    int b = mIn.read();
                    mFrameFin = (b & 0x80) != 0;
                    mFrameOpcode = (b & 0x0F);

                    // MASK, PAYLOAD LEN 1
                    b = mIn.read();
                    mFrameMasked = (b & 0x80) != 0;
                    int framePayloadLen1 = (b & 0x7F);

                    if (mFrameOpcode > 7) { // control frame (ping or pong
                        // control frames MUST NOT be fragmented
                        if (!mFrameFin) {
                            break;
                        }
                        // control frames MUST have payload 125 octets or less
                        if (framePayloadLen1 > 125) {
                            break;
                        }
                        // check for reserved control frame opcodes
                        if (!(mFrameOpcode == OPCODE_PING || mFrameOpcode == OPCODE_PONG)) {
                            break;
                        }
                    } else { // data frame
                        // check for reserved data frame opcodes
                        if (!(mFrameOpcode == OPCODE_CONT || mFrameOpcode == OPCODE_TEXT_FRAME || mFrameOpcode == OPCODE_BINARY_FRAME)) {
                            break;
                        }
                    }

                    if (framePayloadLen1 == 126) {
                        len1 = (byte) mIn.read();
                        len2 = (byte) mIn.read();
                        mFramePayloadLen = (len1 << 8) | (len2 & 0xFF);
                        if (mFramePayloadLen < 126) {
                            break;
                        }
                    } else if (framePayloadLen1 == 127) {
                        len1 = (byte) mIn.read();
                        len2 = (byte) mIn.read();
                        len3 = (byte) mIn.read();
                        len4 = (byte) mIn.read();
                        len5 = (byte) mIn.read();
                        len6 = (byte) mIn.read();
                        len7 = (byte) mIn.read();
                        len8 = (byte) mIn.read();
                        mFramePayloadLen = (len1 << 56) | (len2 << 48)
                                | (len3 << 40) | (len4 << 32)
                                | (len5 << 24) | (len6 << 16)
                                | (len7 << 8) | len8;
                        if (mFramePayloadLen > MAX_FRAME_SIZE) {
                            throw new Exception("freame too long.");
                        }
                        if (mFramePayloadLen < 65536) {
                            break;
                        }
                    } else {
                        mFramePayloadLen = framePayloadLen1;
                    }
                    mCurrentState = STATE_MASKING_KEY;
                    break;
                }
                case STATE_MASKING_KEY: {
                    if (mFrameMasked) {
                        mMaskKeys[0] = (byte) mIn.read();
                        mMaskKeys[1] = (byte) mIn.read();
                        mMaskKeys[2] = (byte) mIn.read();
                        mMaskKeys[3] = (byte) mIn.read();
                    }
                    mPayloadIndex = 0;
                    mPayloadBuffer = new byte[mFramePayloadLen];
                    mCurrentState = STATE_PAYLOAD;
                    break;
                }
                case STATE_PAYLOAD: {
                    mPayloadBuffer[mPayloadIndex] = (byte) mIn.read();
                    if (mFrameMasked) {
                        mPayloadBuffer[mPayloadIndex] ^= mMaskKeys[mPayloadIndex % 4];
                    }
                    mPayloadIndex++;
                    if (mPayloadIndex >= mFramePayloadLen) {
                        mCurrentState = STATE_FRAME_START;
                        byte[] tempBuffer = new byte[mFramePayloadLen];
                        System.arraycopy(mPayloadBuffer, 0, tempBuffer, 0, mFramePayloadLen);
                        mPayloadBuffer = null;
                        WebSocketPacket webSocketPacket = new WebSocketPacket(tempBuffer);
                        if (mFrameOpcode == OPCODE_TEXT_FRAME) {
                            webSocketPacket.setFrameType(WebSocketPacket.FRAMETYPE_UTF8);
                        } else {
                            webSocketPacket.setFrameType(WebSocketPacket.FRAMETYPE_BINARY);
                        }
                        notifyPacket(new WebSocketClientEvent(), webSocketPacket);
                    }
                    break;
                }
                case STATE_CORRUPT: {  //该如何处理数据包混乱？？？没想好
                    mCurrentState = STATE_FRAME_START;
                    break;
                }
                default:
                    throw new Exception("Shouldn't reach here.");
            }  //switch (mCurrentState) {
        } catch (Exception ex) {
            mCurrentState = STATE_CORRUPT;
        }
    }

    public boolean isConnected() {
        return mIsRunning;
    }
    
    public void saveFile(byte[] aData, String aFilename, String aScope, Boolean aNotify) throws WebSocketException {
        /* Token lToken = new Token();
        lToken.put("type", "save");
        lToken.put("ns", NS_FILESYSTEM_PLUGIN);

        lToken.put("sourceId", getClientId());
        lToken.put("sender", getUsername());
        lToken.put("filename", aFilename);
        // TODO: set mimetype correctly according to file extension based on configuration in jWebSocket.xml
        lToken.put("mimetype", "image/jpeg");
        lToken.put("scope", aScope);
        lToken.put("notify", aNotify);

        lToken.put("data", String.valueOf(Tools.base64Encode(aData)));
        sendToken(lToken); */
    }
}
